/*
 * Game project submission for:
 *  Class CSE 486 B of Miami University, Fall 2012
 *  
 * Created by:
 *  Reuben Smith (smithre5)
 *  Michael Jacobs (jacobsm)
 *  Jiang Nuo (jiangn)
 */

/* MIT License
 * 
 * Copyright (c) 2012 Reuben Smith, Michael Jacobs, Jiang Nuo, Miami University
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights 
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 * THE SOFTWARE.
 */

package breakthrough;


import game.GameMove;
import game.GamePlayer;
import game.GameState;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


/**
 * An extremely intelligent Breakthrough AI. You can tell by the use of X.
 * 
 * @author smithre5, jacobsm, jiangn
 */
public class ThreadedXiBreakthroughPlayer extends GamePlayer
{
    /* FIELDS **************************************************************** */

    // smithre5: Since this is static and constant, most Java compilers will
    // optimize away any code wrapped in if (DEBUG) { ... } if it's false.
    // It's a nice way to leave in debug code without a preprocessor.
    private static final boolean    DEBUG              = false;

    private static final int        MOVE_DATA_MASK     = 0x0_FFFFFFF;
    private static final int        MOVE_ROW1_SHIFT    = 0;
    private static final int        MOVE_ROW1_MASK     = 0x7F << MOVE_ROW1_SHIFT;
    private static final int        MOVE_COL1_SHIFT    = 7;
    private static final int        MOVE_COL1_MASK     = 0x7F << MOVE_COL1_SHIFT;
    private static final int        MOVE_ROW2_SHIFT    = 14;
    private static final int        MOVE_ROW2_MASK     = 0x7F << MOVE_ROW2_SHIFT;
    private static final int        MOVE_COL2_SHIFT    = 21;
    private static final int        MOVE_COL2_MASK     = 0x7F << MOVE_COL2_SHIFT;

    // smithre5: These numbers could probably still use some tweaking. MAKE SURE
    // THAT PRUNING AND ADJUSTING ARE TURNED ON AND AT REASONABLES VALUES AT THE
    // TIME OF SUBMISSION. Note to self.
    private static final boolean    SEARCH_ADJUST      = true;
    private static final double     SEARCH_ADJUST_UP   = 0.25;
    private static final double     SEARCH_ADJUST_DOWN = 1.05;
    private static final int        SEARCH_DEPTH_START = SEARCH_ADJUST ? 0 : 4;
    private static final int        SEARCH_DEPTH_STOP  = 32;
    private static final int        SEARCH_PANIC_DEPTH = 4;
    // smithre5: This has a failure case where a long branch is started at 31
    // seconds with a high search depth. This is unlikely to ever be the case
    // since each branch in each worker updates moveTimeRemaining, but it could
    // happen.
    private static final int        SEARCH_PANIC_TIME  = 30;
    private static final boolean    SEARCH_PRUNE       = true;

    // smithre5: availableProcessors() returns the logical processor count as
    // given to the JVM by the OS. This is problematic with processors that have
    // HT since they report HT "cores" also. There's no way to distinguish with
    // this method, so we're just assuming that it has HT if more than 4 cores
    // are reported. (This isn't technically accurate with some of the newer
    // processors. Still need to think of some solution for that.) Might also
    // want to look into rolling back into a single-threaded version for systems
    // reporting one core.
    private static final int        THREAD_COUNT       =
        Math.min(4, Runtime.getRuntime().availableProcessors());

    // smithre5: Most games actually seem to only go out to about 30-40 something
    // moves, but it doesn't hurt (much) to under shoot the time. It'd hurt a lot 
    // more to over shoot it.
    private static final int        AVERAGE_MOVES      = 50;
    // smithre5: Update this as features are added. Bad Things(TM) happen
    // otherwise.
    private static final int        FEATURE_COUNT      = 2;
    private static final int        WIN_SCORE          = Integer.MAX_VALUE;

    private ExecutorService         workerExecutor;
    private ArrayList<SearchWorker> workerList;
    private final Object            workerLock         = new Object();

    private Random                  generator;

    private int                     averageMoves;
    private int                     movesTaken;
    private int                     movesTakenSum;
    private int                     currentRound;

    private int                     gameTimeLimit;
    private double                  gameTimeRemaining;
    private int                     moveTimeLimit;
    private double                  moveTimeRemaining;
    private double                  targetMoveTime;

    private int                     depthLimit;
    private double                  depthAdjustUp;
    private double                  depthAdjustDown;

    private int[]                   featureWeights;

    private String                  opponentName;
    private String                  opponentMessage;

    /* TYPES ***************************************************************** */

    /**
     * Utility class encapsulating points of interest for getStateScore.
     */
    private class EvalVars
    {
        public boolean hasWon;

        public int     goalDelta;
        public int     goalDistance;
        public int     goalRow;

        public int     pieces;
        public int     conflict;
        public int     cover;

        public char    symbol;


        /**
         * Fills variables with appropriate initial values for the side given.
         * 
         * @param side
         *            the side to be evaluated
         */
        public EvalVars(GameState.Who side)
        {
            if (GameState.Who.HOME == side) {
                goalDelta = +1;
                goalRow = BreakthroughState.N - 1;
                symbol = BreakthroughState.homeSym;
            }
            else {
                goalDelta = -1;
                goalRow = 0;
                symbol = BreakthroughState.awaySym;
            }
            
            hasWon = false;
            goalDistance = BreakthroughState.N;
            pieces = 0;
            conflict = 0;
            cover = 0;
        }
    }

    
    /**
     * Defines parameter inputs for AI construction.
     */
    public static class AIParams
    {
        public long  seed;
        public int[] weights;


        public AIParams(long seed, int[] weights)
        {
            this.seed = seed;
            this.weights = weights;
        }
    }


    /* METHODS *************************************************************** */

    /**
     * Default constructor.
     */
    public ThreadedXiBreakthroughPlayer()
    {
        // smithre5: isDeterministic = false?
        super("Xi", new BreakthroughState(), false);

        workerExecutor = Executors.newFixedThreadPool(THREAD_COUNT);
        workerList = new ArrayList<SearchWorker>(THREAD_COUNT);

        generator = new Random();

        // smithre5: Assume all features are in use.
        featureWeights = new int[FEATURE_COUNT];
        for (int i = FEATURE_COUNT; --i >= 0;) {
            featureWeights[i] = 1;
        }
    }


    /**
     * Parameterized AI constructor.
     * 
     * @param params
     *            initial values for AI
     */
    public ThreadedXiBreakthroughPlayer(AIParams params)
    {
        this();

        // This is to allow the user to specify parameters without affecting the
        // randomness of the AI.
        if (params.seed != 0) {
            generator.setSeed(params.seed);
        }

        if (params.weights != null) {
            int fwSize = Math.min(params.weights.length, featureWeights.length);

            for (int i = fwSize; --i >= 0;) {
                featureWeights[i] = params.weights[i];
            }

            for (int i = featureWeights.length - fwSize; --i >= 0;) {
                featureWeights[i] = 0;
            }
        }
    }


    /**
     * Pre-tournament handling.
     */
    public void init()
    {
        averageMoves = AVERAGE_MOVES;
        movesTaken = 0;
        movesTakenSum = 0;
        currentRound = 0;
    }


    /**
     * Post-tournament handling.
     */
    public void done()
    {
        workerExecutor.shutdownNow();
    }


    /**
     * Pre-game handling.
     * 
     * @param opponentName
     *            the name of the opponent
     */
    public void startGame(String opponentName)
    {
        this.opponentName = opponentName;

        depthLimit = SEARCH_DEPTH_START;
        depthAdjustUp = SEARCH_ADJUST_UP;
        depthAdjustDown = SEARCH_ADJUST_DOWN;

        movesTakenSum += movesTaken;
        averageMoves = (averageMoves + movesTakenSum) / ++currentRound;
        movesTaken = 0;

        if (targetMoveTime != 0) {
            gameTimeRemaining = gameTimeLimit;
            targetMoveTime = moveTimeLimit / (double)averageMoves;
        }
    }


    /**
     * Post-game handling. Gives the result for the previous game.
     * 
     * @param result
     *            -1 for loss, 0 for draw, +1 for win
     */
    public void endGame(int result)
    {
    }


    /**
     * Stores message from opponent.
     * 
     * @param message
     *            the message from the opponent
     */
    public void messageFromOpponent(String message)
    {
        this.opponentMessage = message;
    }


    /**
     * Returns a human-readable message for the opponent.
     * 
     * @param opponentName
     *            the name of the opponent
     * @return a (witty?) message to the opponent
     */
    public String messageForOpponent(String opponentName)
    {
        // smithre5: Not necessary, but it's a point for a bit of fun.

        String[] messages =
            new String[] {
                String.format("Hello, %s. Nice to beat you.", opponentName),
                String.format("Why, if it isn't %s.", opponentName),
                String.format(
                    "Did you hear? %s lost in the semi-finals. ...sorry. Didn't see you there.",
                    opponentName),
                String.format("'%s', you say? I'm going to pummel you for that, %s.",
                    opponentMessage, opponentName) 
        };

        return messages[generator.nextInt(messages.length)];
    }


    /**
     * Gives in seconds the time taken for the last move.
     * 
     * @param seconds
     *            the time taken in seconds
     */
    public void timeOfLastMove(double seconds)
    {
        gameTimeRemaining -= seconds;

        if (SEARCH_ADJUST) {
            if (depthLimit < SEARCH_DEPTH_STOP && seconds < depthAdjustUp * targetMoveTime) {
                ++depthLimit;
            }
            else if (seconds > depthAdjustDown * targetMoveTime) {
                --depthLimit;

                if (seconds * AVERAGE_MOVES > depthAdjustDown * gameTimeLimit) {
                    // smithre5: Why by dAD?
                    depthAdjustUp *= 1.0 / depthAdjustDown;
                }
            }
        }
    }


    /**
     * Returns the move to be made next.
     * 
     * This method either performs or invokes the methods necessary to perform
     * the logic in deciding the player's next move.
     * 
     * @param state
     *            the state of the game (board information)
     * @param lastMv
     *            the opponent's move preceding this
     * @return the move to be made
     */
    @Override
    public GameMove getMove(GameState state, String lastMv)
    {
        if (targetMoveTime == 0) {
            // smithre5: Still not 100% on the differences here.
            moveTimeLimit = Math.min(BreakthroughState.gameParams.integer("MOVETIME"),
                BreakthroughState.gameParams.integer("MAXMOVETIME"));

            gameTimeLimit = BreakthroughState.gameParams.integer("GAMETIME");

            targetMoveTime = moveTimeLimit / (double)averageMoves;
        }

        // smithre5: Both sides move, hence 2.
        movesTaken += 2;
        moveTimeRemaining = gameTimeRemaining;

        return getBestMove((BreakthroughState)state);
    }


    /**
     * Returns the move considered best for the given state.
     * 
     * @param state
     *            the state to search from
     * @return the best move, ever
     */
    private BreakthroughMove getBestMove(final BreakthroughState state)
    {
        ArrayList<Integer> moves = getOrderedMoves(getLegalMoveData(state));
        int bestMoveData = DEBUG ? moves.get(0) : moves.get(generator.nextInt(moves.size()));

        if (depthLimit > 0) {
            int bestScore = -WIN_SCORE;
    
            ArrayList<ArrayList<Integer>> buckets = new ArrayList<ArrayList<Integer>>(THREAD_COUNT);
            int bucketIndex = 0;
    
            for (int moveData : moves) {
                ArrayList<Integer> bucket = null;
    
                try {
                    bucket = buckets.get(bucketIndex);
                }
                catch (IndexOutOfBoundsException ex) {
                    bucket = new ArrayList<Integer>(moves.size() / (THREAD_COUNT - 1));
                    buckets.add(bucketIndex, bucket);
                }
    
                bucket.add(moveData);
                bucketIndex = ++bucketIndex % THREAD_COUNT;
            }
    
            workerList.clear();
            for (ArrayList<Integer> bucket : buckets) {
                workerList.add(new SearchWorker((BreakthroughState)state.clone(), bucket));
            }
    
            try {
                List<Future<SearchWorker.Result>> results = workerExecutor.invokeAll(workerList);
                for (Future<SearchWorker.Result> result : results) {
                    if (result.get().score > bestScore) {
                        bestMoveData = result.get().move;
                        bestScore = result.get().score;
                    }
                }
            }
            catch (Exception ex) {
                // smithre5: If interrupted, might be diminished performance 
                // with regards to accuracy. Granted, an interrupt likely means
                // the player is being halted anyway.
            }
        }

        return getMoveFromData(bestMoveData);
    }

    
    // smithre5: Might want to put this into another source file.
    
    /**
     * Worker class for negamax search.
     */
    private class SearchWorker
        implements Callable<SearchWorker.Result>
    {
        private BreakthroughState   state;
        private ArrayList<Integer>  moves;
        private int[]               featureScores;

        
        /**
         * Search results container. Encapsulates useful information about the
         * move selected.
         */
        public class Result
        {
            public int score;
            public int move;


            public Result(int score, int move)
            {
                this.score = score;
                this.move = move;
            }
        }


        /**
         * Creates a worker with the given initial state and allowed moves.
         * 
         * @param state the state to search from
         * @param moves the moves used in the search
         */
        public SearchWorker(final BreakthroughState state, final ArrayList<Integer> moves)
        {
            this.state = state;
            this.moves = moves;
            this.featureScores = new int[FEATURE_COUNT];
        }


        @Override
        public Result call()
        {
            int bestMoveData = DEBUG ? moves.get(0) : moves.get(generator.nextInt(moves.size()));

            if (depthLimit <= 0) {
                state.makeMove(getMoveFromData(bestMoveData));
                return new Result(getStateScore(state), bestMoveData);
            }

            int bestScore = -WIN_SCORE;
            int localDepthLimit = depthLimit;
            BreakthroughMove move = new BreakthroughMove();

            for (int moveData : moves) {
                long moveTimeStart = System.nanoTime();

                BreakthroughState successor = (BreakthroughState)state.clone();
                successor.makeMove(setMoveFromData(move, moveData));

                synchronized(workerLock) {
                    if (moveTimeRemaining <= SEARCH_PANIC_TIME) {
                        localDepthLimit = SEARCH_PANIC_DEPTH;
                    }
                }

                int score = -getMoveScore(successor, localDepthLimit - 1, -WIN_SCORE, WIN_SCORE);

                if (score > bestScore) {
                    bestMoveData = moveData;
                    bestScore = score;
                }

                synchronized(workerLock) {
                    // smithre5: Divide by 1E9 to move from ns to s.
                    moveTimeRemaining -= (System.nanoTime() - moveTimeStart) / 1E9;
                }
            }

            return new Result(bestScore, bestMoveData);
        }


        /**
         * Returns the results of a negamax with alpha-beta search from the
         * given state.
         * 
         * @param state
         *            the state to be searched from
         * @param depth
         *            the remaining depth
         * @param alpha
         *            the alpha cut-off
         * @param beta
         *            the beta cut-off
         * @return the score for the given state
         */
        private int getMoveScore(final BreakthroughState state, int depth, int alpha, int beta)
        {            
            if (depth == 0 || state.getStatus() != GameState.Status.GAME_ON) {
                return getStateScore(state);
            }

            ArrayList<Integer> moves = getOrderedMoves(getLegalMoveData(state));
            BreakthroughMove move = new BreakthroughMove();

            for (int moveData : moves) {
                BreakthroughState successor = (BreakthroughState)state.clone();
                successor.makeMove(setMoveFromData(move, moveData));

                /*
                 * smithre5: Just so I don't forget, here's how the reduction
                 * for this works:
                 * 
                 * 1. foreach (move): 
                 * 2.   score = -getMoveScore( ... ) 
                 * 3.   if (score > a) 
                 * 4.     a = score 
                 * 5.   if (score >= b) 
                 * 6.     return a 
                 * 7. return a
                 * 
                 * We can replace (2) with a = max(a, -getMoveScore( ... ))
                 * because of (3) and (4) -- a is assigned score when score is
                 * bigger than a. This is the same as saying a = a if score < a
                 * or a = score if score >= a.
                 * 
                 * Since score has been transformed into a, we can replace the
                 * use of score in (5) with a, giving us if (a >= b) { ... }.
                 * The return in (6) can be replaced with a break because (7)
                 * already returns a.
                 */

                alpha = Math.max(alpha, -getMoveScore(successor, depth - 1, -beta, -alpha));

                if (SEARCH_PRUNE && alpha >= beta) {
                    break;
                }
            }

            return alpha;
        }


        /**
         * Evaluates the given state and scores it.
         * 
         * @param state
         *            the state to be evaluated
         * @return the given state's score
         */
        private int getStateScore(final BreakthroughState state)
        {            
            EvalVars player = new EvalVars(state.getWho());
            EvalVars opponent = new EvalVars(state.getWho() == GameState.Who.HOME ? 
                GameState.Who.AWAY : GameState.Who.HOME);

            populateEvalVars(state, player, opponent);

            if (player.hasWon) {
                return WIN_SCORE;
            }
            else if (opponent.hasWon) {
                return -WIN_SCORE;
            }

            // smithre5: Why am I doing this? This is brittle. Make sure that
            // f < FEATURE_COUNT holds.
            int f = 0;
            // smithre5: These are separated to allow weighting them differently.
            featureScores[f++] = player.pieces;
            featureScores[f++] = -opponent.pieces;

            int score = 0;
            for (int i = featureScores.length; --i >= 0;) {
                score += featureWeights[i] * featureScores[i];
            }

            return score;
        }


        /**
         * Examines the given state and provides evaluation variable data for
         * both sides.
         * 
         * @param state
         *            the state examined
         * @param player
         *            the current player's perspective
         * @param opponent
         *            the opponent's perspective
         * @return true if population finished successfully; false otherwise
         */
        private boolean populateEvalVars(final BreakthroughState state, EvalVars player,
            EvalVars opponent)
        {
            if (state == null || player == null || opponent == null) {
                return false;
            }

            for (int row = BreakthroughState.N; --row >= 0;) {
                for (int col = BreakthroughState.N; --col >= 0;) {
                    if (state.board[row][col] == player.symbol) {
                        if (player.goalRow == row) {
                            player.hasWon = true;
                            return true;
                        }
                        
                        ++player.pieces;
                        
                        int distance = Math.abs(row - player.goalRow);
                        if (distance < player.goalDistance) {
                            player.goalDistance = distance;
                        }

                        int nextRow = row + player.goalDelta;
                        int rowTest = BreakthroughState.N - nextRow - 1;
                        if (rowTest >= 0 && rowTest < BreakthroughState.N) {
                            int leftCol = col - 1, rightCol = col + 1;

                            if (leftCol >= 0) {
                                if (state.board[nextRow][leftCol] == player.symbol) {
                                    ++player.cover;
                                }
                                else if (state.board[nextRow][leftCol] == opponent.symbol) {
                                    // smithre5: It's cheap and symmetry is pretty, but do we 
                                    // really care about tracking the opponent's conflict? It 
                                    // always equals the player's...
                                    ++player.conflict;
                                    ++opponent.conflict;
                                }
                            }
                            if (rightCol < BreakthroughState.N) {
                                if (state.board[nextRow][rightCol] == player.symbol) {
                                    ++player.cover;
                                }
                                else if (state.board[nextRow][rightCol] == opponent.symbol) {
                                    ++player.conflict;
                                    ++opponent.conflict;
                                }
                            }
                        }
                    }
                    else if (state.board[row][col] == opponent.symbol) {
                        if (opponent.goalRow == row) {
                            opponent.hasWon = true;
                            return true;
                        }
                        
                        ++opponent.pieces;
                        
                        int distance = Math.abs(row - opponent.goalRow);
                        if (distance < opponent.goalDistance) {
                            opponent.goalDistance = distance;
                        }

                        int nextRow = row + opponent.goalDelta;
                        int rowTest = BreakthroughState.N - nextRow - 1;
                        if (rowTest >= 0 && rowTest < BreakthroughState.N) {
                            int leftCol = col - 1, rightCol = col + 1;

                            if (leftCol >= 0) {
                                if (state.board[nextRow][leftCol] == opponent.symbol) {
                                    ++opponent.cover;
                                }
                            }
                            if (rightCol < BreakthroughState.N) {
                                if (state.board[nextRow][rightCol] == opponent.symbol) {
                                    ++opponent.cover;
                                }
                            }
                        }
                    }
                }
            }

            if (player.pieces == 0) {
                opponent.hasWon = true;
            }
            else if (opponent.pieces == 0) {
                player.hasWon = true;
            }

            return true;
        }
    }


    /**
     * Generates a list of all legal moves for the state's active player.
     * 
     * @param state
     *            the game state to be examined
     * @return an ArrayList of legal moves for the given state
     */
    protected static final ArrayList<Integer> getLegalMoveData(final BreakthroughState state)
    {
        // smithre5: Using 4.5 here (or 75%) since most situations will never
        // actually reach b_max. Tests on 1,000 round tournaments only reached
        // 65-70% of b_max. This gives a slight performance boost.
        ArrayList<Integer> moves = new ArrayList<Integer>((int)(4.5f * BreakthroughState.N));

        // Initial values for sanity checks in debugging.
        char pSymbol = '?';
        int pDelta = 0;

        if (state.getWho() == GameState.Who.HOME) {
            pSymbol = BreakthroughState.homeSym;
            pDelta = +1;
        }
        else {
            pSymbol = BreakthroughState.awaySym;
            pDelta = -1;
        }

        for (int row = 0; row < BreakthroughState.N; ++row) {
            for (int col = 0; col < BreakthroughState.N; ++col) {
                // skip non-player pieces
                if (state.board[row][col] != pSymbol) {
                    continue;
                }

                int nextRow = row + pDelta;
                int leftCol = col - 1;
                int rightCol = col + 1;

                // can move piece?
                if (nextRow >= 0 && nextRow < BreakthroughState.N) {
                    char[] rowContents = state.board[nextRow];
                    int data = row << MOVE_ROW1_SHIFT | col << MOVE_COL1_SHIFT | nextRow << MOVE_ROW2_SHIFT;

                    // can move forward?
                    if (rowContents[col] == BreakthroughState.emptySym) {
                        moves.add(data | col << MOVE_COL2_SHIFT);
                    }

                    // can move left?
                    if (leftCol >= 0 && rowContents[leftCol] != pSymbol) {
                        moves.add(data | leftCol << MOVE_COL2_SHIFT);
                    }

                    // can move right?
                    if (rightCol < BreakthroughState.N && rowContents[rightCol] != pSymbol) {
                        moves.add(data | rightCol << MOVE_COL2_SHIFT);
                    }
                }
            }
        }

        return moves;
    }


    /**
     * Performs optimal move ordering on a collection of moves.
     * 
     * @param moves
     *            the moves to be ordered
     * @return a collection of ordered moves
     */
    private static ArrayList<Integer> getOrderedMoves(ArrayList<Integer> moves)
    {
        if (DEBUG) {
            return moves;
        }

        // smithre5: Random move ordering. So not really that optimal.
        Collections.shuffle(moves);

        return moves;
    }
    
    
    /**
     * Creates a BreakthroughMove object from the given move data.
     * 
     * @param data the data encoding the move
     * @return a BreakthroughMove
     */
    private static final BreakthroughMove getMoveFromData(int data)
    {
        data = data & MOVE_DATA_MASK;
        
        return new BreakthroughMove(
            (data & MOVE_ROW1_MASK) >> MOVE_ROW1_SHIFT,
            (data & MOVE_COL1_MASK) >> MOVE_COL1_SHIFT,
            (data & MOVE_ROW2_MASK) >> MOVE_ROW2_SHIFT,
            (data & MOVE_COL2_MASK) >> MOVE_COL2_SHIFT
        );
    }
    
    
    /**
     * Assigns coordinates to a BreakthroughMove object from the given move data.
     * 
     * @param move the move to be used
     * @param data the data to be assigned
     * @return the move given
     */
    private static final BreakthroughMove setMoveFromData(BreakthroughMove move, int data)
    {
        data = data & MOVE_DATA_MASK;
        
        move.startRow = (data & MOVE_ROW1_MASK) >> MOVE_ROW1_SHIFT;
        move.startCol = (data & MOVE_COL1_MASK) >> MOVE_COL1_SHIFT;
        move.endingRow = (data & MOVE_ROW2_MASK) >> MOVE_ROW2_SHIFT;
        move.endingCol = (data & MOVE_COL2_MASK) >> MOVE_COL2_SHIFT;
        
        return move;
    }


    /**
     * Runs the player.
     * 
     * @param args
     *            command-line arguments to the player
     */
    public static void main(String[] args)
    {
        GamePlayer p = new ThreadedXiBreakthroughPlayer();
        p.compete(args);
    }
}
